// vim: fenc=utf8 ff=unix noet ts=4 sts=4 sw=4
// ==UserScript==
// @name           Higu loader
// @namespace      http://code.google.com/p/higulo-tools/
// @description    higuro log autoloader
// @include        http://tkido.com/h_jinro/game.php*
// ==/UserScript==
// スクリプト変数 /////////////////////////////

var version = "0.2.6";

//// Firebug console ////
var LOG_LEVEL = 5;
var group=con("group",0),groupEnd=con("groupEnd",0),error=con("error",1),warn=con("warn",2),info=con("info",3)
var debug=con("debug",4),assert=con("assert",4),log=con("log",3),time=con("time",3),timeEnd=con("timeEnd",5),trace=con("trace",5)
function con(api,level){
    var nop=function() {}
    var c=(unsafeWindow)?unsafeWindow.console:window.console;
    return(!c||!c[api]||LOG_LEVEL<level)?nop:
        function(){c[api].apply(c,Array.slice(arguments))
    }
}

Object.each = function (o, f) {
    for (var k in o) {
        if (o.hasOwnProperty(k)) f(o[k],k,o);
    }
    return o;
}
Array.from = function (a) {
    if (a instanceof Array) return a;
    var x = new Array(a.length);
    for (var i = 0; i < a.length; i++) {
        if (a[i] != undefined) x[i] = a[i];
    }
    return x;
}
Array.each = function (a, f) { return Array.from(a).each(f) }
Array.prototype.each = function (f) {
    for (var i = 0; i < this.length; i++) {
        f(this[i],i,this);
    }
    return this;
}
String.prototype.$f = function () {
    var args = Array.from(arguments);
    return this.replace(/%([%sd])/g, function ($0, $1, idx, self) {
        var r = args.shift();
        return r;
        log("%s,%s = %s", $0, $1, r);
        switch ($1) {
        case "%":
            return "%";
        case "d":
            return parseInt(r);
        case "s":
            return new String(r);
        }
        return $0;
    });
}

// Element methos
function $X(src) {
    if (!node) node = document;
    var r = document.evaluate(xpath,node,null,XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,null);
    return (r.snapshotLength < 1) ? null : r.snapshotItem(0);
}
function query_xpath (xpath, node) {
    if (!node) node = document;
    var r = document.evaluate(xpath,node,null,XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,null);
    return (r.snapshotLength < 1) ? null : r.snapshotItem(0);
}
function build_element (name, attrs, children, doc) {
    if (!doc) doc = document;
    var node = doc.createElement(name);
    if (attrs) set_attributes(node, attrs);
    if (children) append_children(node, children);
    return node;
}
function set_attributes (node, attrs) {
    Object.each(attrs, function (v, k) {
        if (k == "style") {
            if (typeof v == "string") {
                node.style.cssText = v;
            } else {
                Object.each(v, function (a, n) { node[k][n] = a });
            }
        } else if (k.substring(0, 2) == "on") {
            node.addEventListener(k.substring(2), v, true);
        } else {
            node[k] = v;
        }
    });
    return node;
}
function append_children (node, src) {
    Array.each(src, function (c) {
        node.appendChild((typeof c != "string") ? c : node.ownerDocument.createTextNode(c));
    });
    return node;
}
function replace_children(node, src) {
    Array.each(src, function (c, i) {
        if (typeof c == "string") c = node.ownerDocument.createTextNode(c);
        var target = node.childNodes[i];
        if (!target) {
            node.appendChild(c);
        } else {
            node.replaceChild(c, target);
        }
    });
}

// HTMLをDOMオブジェクトで返す
function eval_html(name, html, doc) {
    if (!doc) doc = document;
    var node = doc.createElement(name);
    node.innerHTML = html;
    return node;
}

// 二つの正規表現に挟まれたコンテントを得る
function re_between(re_from, re_to, str) {
    if (str.match(re_from)) {
        str = RegExp.rightContext;
        if (str.match(re_to)) {
            return RegExp.leftContext;
        }
    }
}

// フォームデータをObjectで返す
function formdata(form) {
    var data = {};
    for (var i = 0; i < form.elements.length; i++) {
        var e = form.elements[i];
        if (e.name && e.value) data[e.name] = e.value;
    }
    return data;
}
// クエリデータを返す
function build_query(data) {
    var queries = []
    Object.each(data, function (v, k) {
        queries.push(k+"="+encodeURIComponent(v));
    });
    return queries.join("&");
}


// iframeテキスト入力ボックス
function overlay_text(text) {
    var doc = text.ownerDocument;
    var iframe = build_element("iframe", {
        style:"border:inset 2px #eed;width:"+(text.offsetWidth-4)+"px;height:"+(text.offsetHeight-4)+"px"
    }, null, doc);
    text.style.display="none";
    text.parentNode.insertBefore(iframe, text);
    var idoc = iframe.contentDocument;
    idoc.write('<HTML>'+
        '<HEAD><link rel="stylesheet" type="text/css" href="h_jinro00.css"/></HEAD>'+
        '<BODY lang="ja" class="%s"></BODY>'.$f(doc.body.className)+
        '</HTML>');
    idoc.close();
    var itext = build_element("textarea", {
        className: text.className,
        style: "border:0;margin:0;padding:0;overflow:auto;width:100%;height:100%",
        value: text.value,
    }, null, idoc);
    // 親コントロールにキーイベントを送信
    function relayKeyEvent(evt) {
        text.value = itext.value; // コピペなど特殊操作に対応できないので結局コピー（汗
        var event = document.createEvent("KeyboardEvent");
        event.initKeyEvent(evt.type, evt.bubbles, evt.cencelable, doc.defaultView,
            evt.ctrlKey, evt.altKey, evt.shiftKey, evt.metaKey, evt.keyCode, evt.charCode);
        text.dispatchEvent(event);
    }
    itext.addEventListener("keyup", relayKeyEvent, true);
    itext.addEventListener("keydown", relayKeyEvent, true);
    //itext.addEventListener("keypress", relayKeyEvent, true);
    var iwin = iframe.contentWindow;
    iwin.addEventListener("load", function () {
        idoc.documentElement.style.cssText = "margin:0;padding:0;overflow:hidden;width:100%;height:100%"
        idoc.body.style.cssText = "margin:0;padding:0;overflow:hidden;width:100%;height:100%"
        idoc.body.appendChild(itext);
    }, false);
    return itext;
}

// implements /////////////////////////////////

// main

main();
function main() {
    
    //var pt = new perftimer("");
    // XPath
    var form        = query_xpath("//form");
    var headtable   = query_xpath("//table[@class='main']");
    var headline    = query_xpath("//td[@class='headline']");
    var players     = query_xpath("//table[@class='main']/tbody/tr[2]");
    var titlebox    = query_xpath("//div[@class='place']");
    var title       = query_xpath("//span[@class='title']");
    var logs        = query_xpath("//div[@class='log']");
    var log_body    = query_xpath("//div[@class='log']/table/tbody");
    var foottable   = query_xpath("//table[@class='main'][2]");
    var commandline = query_xpath("//*[@id='command']")
    var cmdtable    = query_xpath("//body/div[3]//table");
    var text        = query_xpath("//textarea[@name='TXTMSG']");
    var place       = query_xpath("//select[@name='place']");
    var l_no        = query_xpath("//select[@name='l_no']");
    //log("resolve XPath=", pt.lap());
    
    // main
    // 一時保存値
    var loadtime = GM_getValue("loadtime", 0); // 0 = OFF / sec
    var log_mode = GM_getValue("l_no", -1); // -1 = 最新
    var header_show = GM_getValue("header_show", false);
    var cmd_show = GM_getValue("cmd_show", false);
    // ゲーム情報の構築
    var info = gameinfo();
    //log("prepare gameinfo=", pt.lap());
    if (!info) return;
    log("自動ログ更新 ひぐローダー "+version+" by mumu");
    
    //log("perform frame");pt.lap();
    // 疑似フレーム化
    var frm_header, frm_main, frm_footer, body;
    var bar_width = window.innerWidth - document.body.offsetWidth; // スクロールバーの幅
    set_attributes(document.documentElement, { style:"height:100%;overflow:hidden" });
    set_attributes(document.body, { style:"height:100%;overflow:hidden" });
    replace_children(document.body, [
        frm_header = build_element("div", {
            style:"z-index:2;position:absolute;left:0px;right:%dpx;overflow:hidden".$f(bar_width),
        }, [headtable]),
        frm_main = build_element("div", { style:"z-index:1;height:100%;overflow:auto" }, [
            body = build_element("div", { style:"margin:0;padding:0" }, [titlebox, logs]),
        ]),
        frm_footer = build_element("div", {
            style:"z-index:3;position:absolute;left:0px;right:%dpx;bottom:0px;overflow:hidden".$f(bar_width),
        }, [form]),
    ]);
    form.style.margin = "0";
    frm_main.scrollTop = 0;
    body.style.paddingTop = frm_header.offsetHeight+"px";
    body.style.paddingBottom = frm_footer.offsetHeight+"px";
    //log("perform frame=", pt.lap());
    text.parentNode.style.height = "140px";
    text.parentNode.style.width = "464px";
    text.style.position = "fixed";
    var y = document.body.clientHeight - (78 + text.offsetHeight)// -  - 30;
    log(y);
    text.style.top = y + "px";

    cmdtable = query_xpath("/html/body/div[3]/form/table/tbody/tr[2]/td/table/tbody");

    // ヘッダ表示
    headline.style.cursor = "pointer";
    headline.addEventListener("click", toggle_header, true);
    show_header();
    function toggle_header() { show_header(!header_show) }
    function show_header(show) {
        time("show_header");
        if (show == undefined) show = header_show;
        var top = frm_main.scrollTop;
        var bottom = frm_main.scrollHeight - frm_main.scrollTop - frm_main.clientHeight;
        players.style.display = (header_show = show) ? "" : "none";
        body.style.paddingTop = frm_header.offsetHeight+"px";
        frm_main.scrollTop = frm_main.scrollHeight - frm_main.clientHeight - bottom;
        if (top < frm_header.offsetHeight) {
            frm_main.scrollTop = top;
        } else {
            frm_main.scrollTop = frm_main.scrollHeight - frm_main.clientHeight - bottom;
        }
        timeEnd("show_header");
    }
    
    // テキストiframe
    //itext = overlay_text(text);
    itext = text;

    // フッタ表示
    var text_height;
    commandline.style.cursor = "pointer";
    commandline.addEventListener("click", toggle_cmd, true);

    show_cmd();
    function toggle_cmd() { show_cmd(!cmd_show) }
    function show_cmd(show) {
        if (show == undefined) show = cmd_show;
        var top = frm_main.scrollTop;
        var bottom = frm_main.scrollHeight - frm_main.scrollTop - frm_main.clientHeight;
        var display = (cmd_show = show) ? "" : "none";
        cmdtable.rows[0].style.display = display;
        cmdtable.rows[1].style.display = display;
        body.style.paddingBottom = frm_footer.offsetHeight+"px";
        var y = document.body.clientHeight - (78 + text.offsetHeight)// -  - 30;
        text.style.top = y + "px";
        if (bottom < frm_footer.offsetHeight) {
            frm_main.scrollTop = frm_main.scrollHeight - frm_main.clientHeight - bottom;
        } else {
            frm_main.scrollTop = top;
        }
        if (show) itext.focus();
    }
    window.addEventListener("resize", function (evt) {
        var y = document.body.clientHeight - (78 + text.offsetHeight)// -  - 30;
        text.style.top = y + "px";
    }, false);

    l_no.value = info.l_no; // ログ選択状態を保持する
    // ログデータ解析
    var logdata = parse_log(logs);
    
    if (!info.log_mode) {
        // 自動更新コントロールをインストール（最新／現在進行中のログ）
        var ctrl = autoctrl();
        frm_footer.appendChild(ctrl);
        show_cmd();
        body.style.paddingBottom = frm_footer.offsetHeight+"px";
        frm_main.scrollTop = frm_main.scrollHeight - frm_main.clientHeight;
    }
    if (info.log_mode || info.l_no > 2) {
        log("過去ログモード", info);
        // 過去ログモード
        document.title = [info.place_name,info.log_name].join(" ");
        // 一時保存値の保存
        form.addEventListener("submit", function () {
            this.TXTMSG.value = ""; // 発言ミス防止ｗ
            GM_setValue("loadtime", loadtime); // 一時的に保存
            GM_setValue("l_no", l_no.value); // 一時的に保存
            GM_setValue("header_show", header_show);
            GM_setValue("cmd_show", cmd_show);
        }, true);
    } else {
        // 最新：タイトルを設定
        document.title = [info.mura_name,info.turn_name,info.place_name].join(" ");
        // 過去ログは別窓で開く
        form.addEventListener("submit", function () {
            var self = this;
            if (l_no.value != -1) {
                this.target = "_blank";
                this.TXTMSG.value = ""; // 発言ミス防止ｗ
                GM_setValue("loadtime", 0); // 新窓なので初期値
                GM_setValue("header_show", false);
                GM_setValue("cmd_show", false);
                // 戻しておく
                window.setTimeout(function () {
                    this.target = "";
                    l_no.value = -1;
                }, 1);
            } else {
                var btn = query_xpath("//input[@type='submit']");
                if (this.TXTMSG.value != "") btn.disabled = true; // 二重発言防止
                GM_setValue("loadtime", loadtime);
                GM_setValue("header_show", header_show);
                GM_setValue("cmd_show", cmd_show);
            }
            GM_setValue("l_no", l_no.value); // 一時的に保存
        }, true);
    }
    // Ctrl+Enter→submit
    text.addEventListener("keyup", function (evt) {
        if (evt.ctrlKey && evt.keyCode == 13) {
            var event = document.createEvent("HTMLEvents");
            event.initEvent("submit", true, true);
            form.dispatchEvent(event);
        }
    }, true);
    
    // ========

    // ゲーム情報
    function gameinfo() {
        var place_name = ["雛見沢村集会所","鬼ヶ淵の底","冥土インヘヴン","固有結界の中"];
        var player_name = ["レナ","梨花","入江","鷹野","大石","葛西","魅音","詩音","沙都子","悟史","圭一","富竹","知恵","ｵﾔｼﾛ"];
        
        var info = {};
        if (!l_no) return;
        var l_no_max = -1;
        Array.each(l_no.options, function (opt) {
            if (opt.value && l_no_max < opt.value) l_no_max = opt.value;
        });
        if (l_no_max >= 0) info.l_no_max = l_no_max;
        if (!headline || !headline.textContent.match(/^◆人狼のなく頃に (第\d+話　『.+』)　(.+)（(.+)）$/)) {
            debug("invalid headline:", headline);
            return null;
        }
        info.mura_name  = RegExp.$1;
        info.turn_name  = RegExp.$2;
        info.phase_name = RegExp.$3;
        switch (info.turn_name) {
        case "綿流し祭り":
            var l_no_max = -1;
            Array.each(l_no.options, function (opt) {
                if (opt.value && l_no_max < opt.value) l_no_max = opt.value;
            });
            if (l_no_max >= 0) info.l_no_max = l_no_max;
            info.t_no = 0; break;
        case "お疲れ様会":
            info.t_no = -1; break;
        default:
            if (info.turn_name.match(/(\d)日目/)) {
            info.t_no = RegExp.$1;
            } else {
                log("unknown turn_name:", info.turn_name);
            }
        }
        switch (info.phase_name) {
        case "昼":
            info.t_phase = 0; break;
        case "夕方":
            info.t_phase = 1; break;
        case "夜":
            info.t_phase = 2; break;
        default:
            log("unknown phase_name:", info.phase_name);
        }
        if (!title || !title.textContent.match(/～(.+)　(.+)～/)) {
            log("invalid title:", title);
            return null;
        }
        info.place_name = RegExp.$1;
        info.log_name   = RegExp.$2;
        log(info);
        switch (info.place_name) {
        case "雛見沢村集会所":
            info.place = 0;
            info.available = (info.t_phase == 0);
            info.log_mode = (info.l_no >= 0 && info.l_no < info.l_no_max);
            break;
        case "鬼ヶ淵の底":
            info.place = 1;
            info.available = (info.t_no >= 0 && info.t_phase < 2);
            info.log_mode = (info.t_no < info.l_no);
            break;
        case "冥土インヘヴン":
            info.place = 2;
            info.available = (info.t_no >= 0 && info.t_phase < 2);
            break;
        case "固有結界の中":
            var player_name = ["レナ","梨花","入江","鷹野","大石","葛西","魅音","詩音","沙都子","悟史","圭一","富竹","知恵","ｵﾔｼﾛ"];
            player_name.each(function (name, i) {
                if (name == info.log_name) {
                    info.place = 3 + i;
                    return Array._break;
                }
            });
            if (info.place >= 3) {
                info.available = (info.t_no >= 0);
                break;
            }
        default:
            log("unknown place_name =", info.place_name);
        }
        if (info.place >= 3) {
            info.l_no = log_mode;
            info.log_mode = false;
        } else if (info.log_name == "最新") {
            info.l_no = -1;
            info.log_mode = (info.place != 0 && info.t_no < 0);
        } else if (info.log_name.match(/(\d)日目/)) {
            info.l_no = RegExp.$1;
            info.log_mode = (info.t_no <= 0) ?
                (info.place != 0 || info.l_no_max > info.l_no):
                (info.t_no > info.l_no);
        } else {
            log("unknown log_name=", info.log_name);
        }
        //log("info=",info);
        return info;
    }
    
    // 制御パネル作成
    function autoctrl() {
        var msg; // メッセージ
        return build_element("div", { style:{ backgroundColor:"#fff", textAlign:"left", width:titlebox.offsetWidth+"px"}},[
            btn = build_element("button", { onclick:request_log }, ["ログ更新"]),
            " ",
            autobtn(0),
            autobtn(30),
            autobtn(60),
            autobtn(120),
            " ",
            msg = build_element("span", { style:{ fontSize:"small" }}),
        ]);
        
        // 自動ロード制御ボタン
        function autobtn(sec) {
            var btn, ctrl = build_element("span", {}, [
                btn = build_element("input", {
                    id:"auto"+sec,
                    type:"radio",
                    name:"auto",
                    value:sec,
                    onclick:autoload
                }),
                build_element("label", { htmlFor:"auto"+sec }, [(sec == 0)?"OFF":""+sec]),
            ]);
            if (sec == loadtime) {
                btn.checked = "checked";
                autoload.apply(btn);
            }
            return ctrl;
        }
        
        // インターバル
        var timerid;
        function autoload() {
            loadtime = this.value;
            if (timerid != undefined) clearInterval(timerid);
            if (loadtime > 0) {
                timerid = window.setInterval(function () {
                    if (loadtime > 0) request_log();
                }, loadtime * 1000);
            }
        }

        // ログ更新
        function request_log () {
            msg.textContent = "リクエスト中 ...";
            var postdata = formdata(form);
            // 自動更新用パラメータ
            postdata.place = info.place;
            postdata.l_no = info.l_no;
            postdata.COMMAND = "MSG";
            delete postdata.TXTMSG; // TXTMSGを削除しないと二重・三重投稿に（汗
            var url = form.action;
            GM_xmlhttpRequest({
                method: "POST",
                url: url,
                data: build_query(postdata),
                headers: {
                    Cookie: document.cookie,
                    "Content-Type" : "application/x-www-form-urlencoded",
                },
                overrideMimeType:"text/html; charset=EUC-JP",
                onload: function (r) {
                    if (r.status != 200) {
                        error("Status: %s %s;Headers: %s", r.status, r.statusText, r.responseHeders);
                    } else {
                        load_log(r.responseText);
                    }
                },
            });
        }
        // HTML解析
        function load_log(html) {
            msg.textContent += html.length + " 解析中 ...";
            var info = analyze_log(html);
            if (!info) { log(html); return }
            // ログ解析
            var doc = eval_html("div", info.logtext);
            var newlog = append_log(logdata, parse_log(doc));
            if (!newlog.length) {
                msg.textContent += "発言なし";
            } else {
                var bottom = frm_main.scrollHeight - frm_main.scrollTop - frm_main.clientHeight;
                msg.textContent += newlog.length + " 発言追加";
                newlog.each(function (m) { append_children(log_body, m.ref) });
                // ヘッダ差し替え
                var doc = eval_html("div", info.headtext);
                
                var head = doc.firstChild;
                var headtext = head.rows[0].cells[0].textContent;
                if (headline.textContent != headtext) {
                    headline.textContent = headtext;
                }
                var players_new = head.rows[1];
                replace_children(players, players_new.cells);
                // bottom を維持
                if (bottom < frm_footer.offsetHeight) {
                    frm_main.scrollTop = frm_main.scrollHeight - frm_main.clientHeight - bottom;
                }
            }
            commandline.textContent = info.commandtext; // ヘッドライン更新
            if (title.textContent != info.titletext) { // タイトル更新
                title.textContent = info.titletext;
            }
        }
    }
// ログ解析
    function parse_log(doc) {
   
        var logdata = [];
        var tables = doc.getElementsByTagName("table");

        var rows = tables[0].rows;
        for (var i = 0; i < rows.length; i+=2) {
            var m = {
                name: rows[i].cells[0].childNodes[0].textContent,
                time: rows[i].cells[0].childNodes[2].textContent,
                msg: rows[i+1].cells[2].childNodes[0].textContent,
                ref: [rows[i], rows[i+1]],
            }
            if (m.time.match(/\(\s*(\d+)回目\)/)) {
                m.cnt = RegExp.$1;
            }
            if (m.time.match(/\s*(\d+)時(\d+)分　*(\d+)\/(\d+)\/(\d+)\(.+\)/)) {
                m.date = new Date(RegExp.$3, RegExp.$4, RegExp.$5, RegExp.$1, RegExp.$2);
            }
            logdata.push(m);
        }
        return logdata;
    }
}

// 既得のログに追加
function append_log(logdata, src) {
    var i = 0;
    if (logdata.length > 0) {
        var lastmsg = logdata[logdata.length -1];
        for (var i = src.length -1; i >= 0; i--) {
            if (src[i].name == lastmsg.name && src[i].cnt == lastmsg.cnt) {
                i++;
                break;
            }
        }
    }
    var newlog = src.slice(i);
    newlog.each(function (m) {
        log(m);
    logdata.push(m) });
    return newlog;
}

// HTMLソースを解析する
function analyze_log(content) {
    var info = {};
    info.headtext = re_between(/<form.*?>\s*/i, /\s*<div class=place>/i, content);
    if (!info.headtext) { log("no headline",info.headtext); return }
    info.titletext = re_between(/<span class=title>/i, /<\/span>/i, content);
    if (!info.titletext) { log("no title", info.titletext); return }
    info.commandtext = re_between(/<td id=command>\s*/i, /\s*<\/td>/i, content);
    if (!info.commandtext) { log("no command", info.commandtext); return }
    info.logtext = re_between(/<div class=log>/i, /<\/div><table class=main>/i, content);
    if (!info.logtext) { log("no log", info.logtext); return }
    return info;
}

///////////////////////
